Skip to content

fix(epoxy): make quorum sizes match paper#4234

Merged
NathanFlurry merged 2 commits intomainfrom
02-19-fix_epoxy_make_quorum_sizes_match_paper
Feb 27, 2026
Merged

fix(epoxy): make quorum sizes match paper#4234
NathanFlurry merged 2 commits intomainfrom
02-19-fix_epoxy_make_quorum_sizes_match_paper

Conversation

@MasterPtato
Copy link
Contributor

Description

Please include a summary of the changes and the related issue. Please also include relevant motivation and context.

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

How Has This Been Tested?

Please describe the tests that you ran to verify your changes.

Checklist:

  • My code follows the style guidelines of this project
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes

@railway-app
Copy link

railway-app bot commented Feb 19, 2026

🚅 Deployed to the rivet-pr-4234 environment in rivet-frontend

Service Status Web Updated (UTC)
website 😴 Sleeping (View Logs) Web Feb 23, 2026 at 8:48 pm
ladle ❌ Build Failed (View Logs) Web Feb 21, 2026 at 1:41 am
frontend-inspector ❌ Build Failed (View Logs) Web Feb 19, 2026 at 8:48 pm
mcp-hub ✅ Success (View Logs) Web Feb 19, 2026 at 8:48 pm
frontend-cloud ❌ Build Failed (View Logs) Web Feb 19, 2026 at 8:48 pm

Copy link
Contributor Author

MasterPtato commented Feb 19, 2026

@claude
Copy link

claude bot commented Feb 19, 2026

PR Review: fix(epoxy): make quorum sizes match paper

Summary

This PR separates the fanout quorum calculation into its own function (calculate_fanout_quorum) and adds proper edge-case handling for small cluster sizes. The code cleanup in http_client.rs (removing glob imports, simplifying std::result::Result::Ok to Ok) is good and follows the project's conventions.

However, there's a significant concern with the fast quorum formula that deserves attention before merge.


Critical: Fast quorum formula may be smaller than EPaxos section 4.3 requires

The comment cites EPaxos §4.3, which specifies fast quorum size as ⌊3N/4⌋ + 1. The old formula matched this exactly:

// Old (correct per EPaxos §4.3)
QuorumType::Fast => (n * 3) / 4 + 1,

The new formula uses f + (f + 1) / 2 (integer division) where f = (n - 1) / 2:

N f Old ⌊3N/4⌋+1 New f+(f+1)/2 EPaxos min (> (N+f)/2)
3 1 3 2 3
5 2 4 3 4
7 3 6 5 6
9 4 7 6 7

The new formula consistently produces fast quorums 1 smaller than the paper's requirement. The EPaxos safety argument depends on any two fast quorums intersecting in at least f+1 nodes (so recovery can always find a node that participated). A fast quorum that is one too small violates this intersection property.

Concrete impact in the accept phase — in send_pre_accepts, the fanout waits for calculate_fanout_quorum(n, Fast) responses before deciding the path. For N=5 the old code waited for 3 other replicas (quorum of 4 including sender), but the new code waits for only 2. If those 2 agree, decide_path takes the fast path — but EPaxos requires 3 other agreements (4 total) before that is safe.

Could you clarify which paper edition this formula comes from, or point to the theorem that justifies the smaller values? If the protocol has diverged from stock EPaxos, a note in the code would help future readers.


Minor: QuorumType::Any inconsistency in calculate_fanout_quorum

For n >= 3, calculate_fanout_quorum returns 1 for Any, meaning the fanout waits for one additional replica. But calculate_quorum(n, Any) returns 1 (the sender alone satisfies the quorum), so the fanout should be 0. These two functions give inconsistent answers for Any.

Concretely in get_optimistic.rs, the caller already guards the n == 1 case, so in practice n >= 2 when fanout_to_replicas is called with Any. For n == 2, the fanout correctly requires the one other replica to respond. For n >= 3 it waits for one arbitrary replica, which appears to be the intended semantics for an optimistic read ("confirm with any other node"). If that is intended, the docstring on calculate_quorum for Any should be clarified, since its return value of 1 implies the sender alone suffices, which conflicts with what calculate_fanout_quorum does.


Missing unit tests

Given that this is a correctness-critical fix to consensus quorum sizes, a table-driven unit test (e.g., verifying expected values for calculate_quorum and calculate_fanout_quorum at N = 1, 2, 3, 5, 7) would provide a clear specification and catch regressions. Even something simple in utils.rs would help a lot here:

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn quorum_sizes_match_epaxos() {
        // (n, fast, slow)
        let cases = [(3, 3, 2), (5, 4, 3), (7, 6, 4)];
        for (n, expected_fast, expected_slow) in cases {
            assert_eq\!(calculate_quorum(n, QuorumType::Fast), expected_fast, "n={n}");
            assert_eq\!(calculate_quorum(n, QuorumType::Slow), expected_slow, "n={n}");
        }
    }
}

Positive changes

  • Removing the glob import from anyhow follows the CLAUDE.md convention.
  • Simplifying std::result::Result::OkOk is cleaner.
  • The early return when target_responses == 0 is a good guard (though warn\! level seems noisy for the single-node case; debug\! might be more appropriate).
  • Extracting calculate_fanout_quorum makes the "sender is excluded" semantic explicit rather than having it buried in a match arm.
  • The n == 0, n == 1, n == 2 edge-case handling is a good addition.

@pkg-pr-new
Copy link

pkg-pr-new bot commented Feb 19, 2026

More templates

@rivetkit/cloudflare-workers

pnpm add https://pkg.pr.new/rivet-dev/rivet/@rivetkit/cloudflare-workers@4234

@rivetkit/framework-base

pnpm add https://pkg.pr.new/rivet-dev/rivet/@rivetkit/framework-base@4234

@rivetkit/next-js

pnpm add https://pkg.pr.new/rivet-dev/rivet/@rivetkit/next-js@4234

@rivetkit/react

pnpm add https://pkg.pr.new/rivet-dev/rivet/@rivetkit/react@4234

rivetkit

pnpm add https://pkg.pr.new/rivet-dev/rivet/rivetkit@4234

@rivetkit/sql-loader

pnpm add https://pkg.pr.new/rivet-dev/rivet/@rivetkit/sql-loader@4234

@rivetkit/sqlite-vfs

pnpm add https://pkg.pr.new/rivet-dev/rivet/@rivetkit/sqlite-vfs@4234

@rivetkit/traces

pnpm add https://pkg.pr.new/rivet-dev/rivet/@rivetkit/traces@4234

@rivetkit/workflow-engine

pnpm add https://pkg.pr.new/rivet-dev/rivet/@rivetkit/workflow-engine@4234

@rivetkit/virtual-websocket

pnpm add https://pkg.pr.new/rivet-dev/rivet/@rivetkit/virtual-websocket@4234

@rivetkit/engine-runner

pnpm add https://pkg.pr.new/rivet-dev/rivet/@rivetkit/engine-runner@4234

@rivetkit/engine-runner-protocol

pnpm add https://pkg.pr.new/rivet-dev/rivet/@rivetkit/engine-runner-protocol@4234

commit: 0027acd

@MasterPtato MasterPtato force-pushed the 02-19-fix_epoxy_make_quorum_sizes_match_paper branch from 788e9db to e7553c7 Compare February 19, 2026 21:14
@railway-app railway-app bot temporarily deployed to rivet-frontend / rivet-pr-4234 February 19, 2026 21:14 Destroyed
@MasterPtato MasterPtato marked this pull request as ready for review February 19, 2026 21:14
@MasterPtato MasterPtato force-pushed the 02-17-fix_gas_gracefully_handle_corrupt_wf branch from c7242fc to e418d91 Compare February 19, 2026 22:41
@MasterPtato MasterPtato force-pushed the 02-19-fix_epoxy_make_quorum_sizes_match_paper branch from e7553c7 to 710de07 Compare February 19, 2026 22:41
@railway-app railway-app bot temporarily deployed to rivet-frontend / rivet-pr-4234 February 19, 2026 22:41 Destroyed
@MasterPtato MasterPtato force-pushed the 02-17-fix_gas_gracefully_handle_corrupt_wf branch from e418d91 to 0e4f8dc Compare February 19, 2026 22:51
@MasterPtato MasterPtato force-pushed the 02-19-fix_epoxy_make_quorum_sizes_match_paper branch from 710de07 to fbd667c Compare February 19, 2026 22:53
@MasterPtato MasterPtato force-pushed the 02-17-fix_gas_gracefully_handle_corrupt_wf branch from 0e4f8dc to 233582b Compare February 19, 2026 22:53
@railway-app railway-app bot temporarily deployed to rivet-frontend / rivet-pr-4234 February 19, 2026 22:53 Destroyed
@graphite-app graphite-app bot changed the base branch from 02-17-fix_gas_gracefully_handle_corrupt_wf to graphite-base/4234 February 19, 2026 22:55
@graphite-app graphite-app bot force-pushed the graphite-base/4234 branch from 233582b to 682f215 Compare February 19, 2026 22:56
@railway-app railway-app bot temporarily deployed to rivet-frontend / rivet-pr-4234 February 19, 2026 22:56 Destroyed
@graphite-app graphite-app bot force-pushed the 02-19-fix_epoxy_make_quorum_sizes_match_paper branch from fbd667c to 6b4b34d Compare February 19, 2026 22:56
@graphite-app graphite-app bot changed the base branch from graphite-base/4234 to main February 19, 2026 22:57
@graphite-app graphite-app bot force-pushed the 02-19-fix_epoxy_make_quorum_sizes_match_paper branch from 6b4b34d to 5973cf6 Compare February 19, 2026 22:57
@railway-app railway-app bot temporarily deployed to rivet-frontend / rivet-pr-4234 February 19, 2026 22:57 Destroyed
@MasterPtato MasterPtato force-pushed the 02-19-fix_epoxy_make_quorum_sizes_match_paper branch from 5973cf6 to e86f64c Compare February 20, 2026 00:04
@railway-app railway-app bot temporarily deployed to rivet-frontend / rivet-pr-4234 February 20, 2026 00:04 Destroyed
@MasterPtato MasterPtato force-pushed the 02-19-fix_epoxy_make_quorum_sizes_match_paper branch from e86f64c to a4fe6de Compare February 20, 2026 00:16
@railway-app railway-app bot temporarily deployed to rivet-frontend / rivet-pr-4234 February 20, 2026 00:16 Destroyed
@MasterPtato MasterPtato force-pushed the 02-19-fix_epoxy_make_quorum_sizes_match_paper branch from a4fe6de to 754e84e Compare February 21, 2026 01:40
@railway-app railway-app bot temporarily deployed to rivet-frontend / rivet-pr-4234 February 21, 2026 01:40 Destroyed
@MasterPtato MasterPtato force-pushed the 02-19-fix_epoxy_make_quorum_sizes_match_paper branch from 754e84e to 4e3fd8f Compare February 26, 2026 01:12
@MasterPtato MasterPtato force-pushed the 02-19-fix_epoxy_make_quorum_sizes_match_paper branch from 4e3fd8f to e144ca2 Compare February 26, 2026 02:14
@MasterPtato MasterPtato changed the base branch from main to graphite-base/4234 February 26, 2026 19:42
@MasterPtato MasterPtato force-pushed the 02-19-fix_epoxy_make_quorum_sizes_match_paper branch from e144ca2 to f01fcb4 Compare February 26, 2026 19:42
@MasterPtato MasterPtato changed the base branch from graphite-base/4234 to 02-13-fix_rivetkit_stall_stop_handler_until_start_completes February 26, 2026 20:05
@MasterPtato MasterPtato force-pushed the 02-13-fix_rivetkit_stall_stop_handler_until_start_completes branch from 02b2c93 to 2d6e590 Compare February 27, 2026 00:12
@MasterPtato MasterPtato force-pushed the 02-19-fix_epoxy_make_quorum_sizes_match_paper branch from f01fcb4 to 0027acd Compare February 27, 2026 00:12
Base automatically changed from 02-13-fix_rivetkit_stall_stop_handler_until_start_completes to main February 27, 2026 00:29
@NathanFlurry NathanFlurry merged commit ccd1b0f into main Feb 27, 2026
14 of 24 checks passed
@NathanFlurry NathanFlurry deleted the 02-19-fix_epoxy_make_quorum_sizes_match_paper branch February 27, 2026 00:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants